Popular Searches
Popular Course Categories
Popular Courses

Applying Closures, Higher-Order Functions, and Generics

Applying Closures, Higher-Order Functions, and Generics

Object-Oriented Swift and Advanced Language Features

Applying Closures, Higher-Order Functions, and Generics in Swift

Course: iOS App Development

Topic: Applying Closures, Higher-Order Functions, and Generics in Swift

Course Link: iOS App Development Training at JustAcademy

Demo Link: Register for a Course Demo

JustAcademy Website: Visit JustAcademy


1. Introduction

Swift provides powerful language features that help developers write concise, reusable, and maintainable code. Three important features are closures, higher-order functions, and generics. Closures allow blocks of code to be stored, passed, and executed when required. Higher-order functions accept closures as arguments or return functions as results. Generics allow developers to write flexible functions, structures, classes, and other types that work with different data types while preserving type safety.

These features are widely used in iOS development for sorting data, filtering lists, transforming API responses, handling button actions, processing collections, building reusable components, and implementing asynchronous operations.

2. Learning Objectives

  • Understand closures and their syntax in Swift.
  • Use closure expressions, shorthand arguments, and trailing closures.
  • Understand value capture, escaping closures, and capture lists.
  • Apply higher-order functions such as map, filter, reduce, and sorted.
  • Understand generic functions, generic types, and type constraints.
  • Combine closures, higher-order functions, and generics in practical iOS applications.
  • Identify common errors and follow best practices for readable Swift code.

3. Understanding Closures in Swift

3.1 What Is a Closure?

A closure is a self-contained block of functionality that can be stored in a variable, passed to a function, or executed later. Closures are similar to anonymous functions or lambda expressions in other programming languages. A closure can capture constants and variables from the surrounding scope in which it was created.

Closures are useful when an operation needs custom behavior, such as sorting an array, responding to a user action, or processing a network request after it completes.

3.2 Basic Closure Syntax

A closure expression follows this general structure:

{ (parameters) -> ReturnType in
    // Statements
}
  • Parameters: Values supplied to the closure.
  • Return type: The type of value produced by the closure.
  • in: Separates the parameter and return-type declaration from the closure body.
  • Statements: The code executed when the closure is called.

3.3 Creating and Calling a Closure

let greeting = {
    print("Welcome to Swift programming!")
}

greeting()
// Output: Welcome to Swift programming!

In this example, greeting stores a closure that prints a message. The closure executes when greeting() is called.

3.4 Closures with Parameters and Return Values

let addNumbers = { (a: Int, b: Int) -> Int in
    return a + b
}

let result = addNumbers(10, 20)
print(result)
// Output: 30

This closure accepts two integers and returns their sum. Swift can infer closure types from the context in which they are used, so explicit type annotations are not always required.

3.5 Type Inference and Implicit Return

let multiply = { (a: Int, b: Int) in
    a * b
}

print(multiply(4, 5))
// Output: 20

Because the closure contains a single expression, Swift implicitly returns its result. The inferred return type is Int.

3.6 Shorthand Argument Names

Swift allows closure parameters to be referenced through shorthand names such as $0, $1, and $2. These represent the first, second, and third arguments respectively.

let subtract = { $0 - $1 }

print(subtract(15, 5))
// Output: 10

Shorthand arguments are useful for short, straightforward closures. For complex operations, descriptive parameter names generally improve readability.

3.7 Trailing Closure Syntax

When a closure is the final argument of a function, Swift allows it to be written outside the function's parentheses. This is called trailing closure syntax.

func performOperation(
    a: Int,
    b: Int,
    operation: (Int, Int) -> Int
) -> Int {
    operation(a, b)
}

let result = performOperation(a: 8, b: 4) { x, y in
    x * y
}

print(result)
// Output: 32

Trailing closures are commonly used in SwiftUI view modifiers, animation blocks, collection operations, and completion handlers.

4. Capturing Values in Closures

A closure can capture constants and variables from the surrounding scope. Captured values remain available to the closure even after the function that created it has returned.

func makeCounter() -> () -> Int {
    var count = 0

    return {
        count += 1
        return count
    }
}

let counter = makeCounter()

print(counter()) // 1
print(counter()) // 2
print(counter()) // 3

The returned closure retains access to the captured count variable. Each call updates the same captured state.

4.1 Capture Lists

A capture list controls how values are captured when the closure is created. It is especially useful for controlling references to class instances.

var score = 10

let printScore = { [score] in
    print(score)
}

score = 50
printScore()
// Output: 10

Here, the capture list captures the value of score when the closure is created. The closure prints the captured value rather than the later value of the variable.

4.2 Weak and Unowned References

When a closure stored by a class instance captures that same instance strongly, a strong reference cycle can occur. Capture lists using [weak self] or, where lifetime guarantees make it safe, [unowned self] can help manage references.

class ProfileViewModel {
    var onUpdate: (() -> Void)?

    func configure() {
        onUpdate = { [weak self] in
            guard let self else { return }
            self.refreshProfile()
        }
    }

    func refreshProfile() {
        print("Profile refreshed")
    }
}

weak creates an optional reference that can become nil. Use unowned only when the referenced object is guaranteed to remain alive whenever the closure uses it.

5. Escaping and Non-Escaping Closures

5.1 Non-Escaping Closures

A non-escaping closure is executed before the function that receives it returns. Closure parameters are non-escaping by default unless declared otherwise.

func executeImmediately(action: () -> Void) {
    action()
}

executeImmediately {
    print("Executed immediately")
}

5.2 Escaping Closures

An escaping closure may be stored or executed after the function that receives it has returned. Use the @escaping attribute when a closure parameter is allowed to escape.

var completionTasks: [() -> Void] = []

func saveTask(task: @escaping () -> Void) {
    completionTasks.append(task)
}

saveTask {
    print("Task completed later")
}

completionTasks.first?()
// Output: Task completed later

Escaping closures are common in asynchronous operations, event handling, delayed tasks, and callback-based APIs. Consider the closure's execution timing and memory ownership carefully.

6. Understanding Higher-Order Functions

A higher-order function is a function that accepts another function or closure as an argument, returns a function, or does both. Swift's standard library provides higher-order methods that make collection processing expressive and concise.

Function Purpose Typical Result
map Transforms each element. A new collection of transformed values.
filter Selects elements that satisfy a condition. A collection containing matching elements.
reduce Combines elements into one accumulated result. A single value, such as a sum or string.
sorted Orders elements according to a comparison closure. A new sorted array.
compactMap Transforms elements and removes nil results. An array of non-optional transformed values.
flatMap Transforms elements and flattens nested sequences. A flattened collection.
forEach Executes a closure for each element. No transformed collection is returned.

7. Using map()

The map method applies a transformation to every element of a collection and returns the transformed elements in a new array.

let numbers = [1, 2, 3, 4, 5]

let squares = numbers.map { number in
    number * number
}

print(squares)
// Output: [1, 4, 9, 16, 25]

Example: Converting Names to Uppercase

let names = ["Amit", "Riya", "Neha"]

let uppercasedNames = names.map {
    $0.uppercased()
}

print(uppercasedNames)
// Output: ["AMIT", "RIYA", "NEHA"]

Use case: Transform API data into display labels, convert prices to formatted strings, or create view models from model objects.

8. Using filter()

The filter method returns the elements that satisfy a Boolean condition. The original collection is not modified.

let numbers = [10, 15, 20, 25, 30]

let evenNumbers = numbers.filter { number in
    number % 2 == 0
}

print(evenNumbers)
// Output: [10, 20, 30]

Example: Filtering Active Users

struct User {
    let name: String
    let isActive: Bool
}

let users = [
    User(name: "Amit", isActive: true),
    User(name: "Riya", isActive: false),
    User(name: "Neha", isActive: true)
]

let activeUsers = users.filter { $0.isActive }

for user in activeUsers {
    print(user.name)
}
// Output:
// Amit
// Neha

Use case: Filter products by price, display active users, find available appointments, or show tasks that remain incomplete.

9. Using reduce()

The reduce method combines a collection's elements into a single accumulated value. It starts with an initial value and applies a closure to the accumulated result and each element.

let numbers = [1, 2, 3, 4, 5]

let total = numbers.reduce(0) { partialResult, number in
    partialResult + number
}

print(total)
// Output: 15

Example: Calculating a Shopping Cart Total

let prices = [199.0, 299.0, 150.0]

let totalPrice = prices.reduce(0.0, +)

print(totalPrice)
// Output: 648.0

For financial applications, use an appropriate representation such as integer minor currency units or a suitable decimal type when precision is important.

10. Using sorted()

The sorted method creates a new array ordered according to a comparison closure.

let scores = [45, 90, 65, 30, 80]

let ascendingScores = scores.sorted { $0 < $1 }
let descendingScores = scores.sorted { $0 > $1 }

print(ascendingScores)
// Output: [30, 45, 65, 80, 90]

print(descendingScores)
// Output: [90, 80, 65, 45, 30]

Sorting Custom Objects

struct Student {
    let name: String
    let marks: Int
}

let students = [
    Student(name: "Amit", marks: 82),
    Student(name: "Riya", marks: 95),
    Student(name: "Neha", marks: 76)
]

let rankedStudents = students.sorted {
    $0.marks > $1.marks
}

for student in rankedStudents {
    print("\(student.name): \(student.marks)")
}

This approach is useful for sorting product prices, student marks, task priorities, or search results.

11. Using compactMap() and flatMap()

11.1 compactMap()

compactMap transforms elements and discards results that are nil. It is useful when converting values that may be invalid.

let values = ["10", "20", "hello", "30"]

let numbers = values.compactMap { Int($0) }

print(numbers)
// Output: [10, 20, 30]

11.2 flatMap()

For sequences, flatMap can transform each element into a sequence and flatten the resulting nested sequences into one level.

let groups = [[1, 2], [3, 4], [5, 6]]

let allNumbers = groups.flatMap { $0 }

print(allNumbers)
// Output: [1, 2, 3, 4, 5, 6]

Choose the method according to the desired result: use compactMap to discard failed optional transformations and sequence flatMap to flatten nested sequences.

12. Chaining Higher-Order Functions

Higher-order functions can be combined to express multiple collection-processing steps clearly.

let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

let result = numbers
    .filter { $0 % 2 == 0 }
    .map { $0 * $0 }
    .reduce(0, +)

print(result)
// Output: 220

In this example:

  1. filter keeps even numbers: [2, 4, 6, 8, 10].
  2. map squares each value: [4, 16, 36, 64, 100].
  3. reduce adds the squared values to produce 220.

Chaining makes the data transformation pipeline easy to follow. For very large data sets or complex pipelines, consider performance, intermediate allocations, and whether a loop or lazy sequence is more suitable.

13. Introduction to Generics

Generics allow developers to create reusable functions and types that work with different data types without sacrificing compile-time type safety. Instead of writing separate implementations for integers, strings, and other types, a generic implementation can work with any type that meets its requirements.

13.1 Why Generics Are Useful

  • Reduce repeated code.
  • Preserve type safety at compile time.
  • Improve reusability and maintainability.
  • Support reusable data structures and algorithms.
  • Allow custom types to participate in shared operations through constraints and protocols.

14. Creating a Generic Function

A generic function declares one or more type parameters inside angle brackets. The compiler determines the concrete types from the arguments and context.

func displayValue<T>(_ value: T) {
    print(value)
}

displayValue(100)
displayValue("Hello Swift")
displayValue(true)

Here, T is a placeholder type. It is not a specific data type; the function can be called with different types.

14.1 Generic Function with a Return Value

func makePair<A, B>(_ first: A, _ second: B) -> (A, B) {
    return (first, second)
}

let pair = makePair("iOS", 2026)
print(pair.0)
print(pair.1)

This function accepts two values of potentially different types and returns a tuple preserving each value's type.

15. Building a Generic Swap Function

A generic function can operate on values without knowing their concrete type in advance.

func swapValues<T>(_ a: inout T, _ b: inout T) {
    let temporary = a
    a = b
    b = temporary
}

var first = 10
var second = 20

swapValues(&first, &second)

print(first)  // 20
print(second) // 10

The inout parameters allow the function to modify the original variables. Both arguments must have the same type because they share the generic type parameter T.

16. Creating Generic Structures

Generic structures are reusable data containers that can store values of different types.

struct Box<T> {
    var value: T

    func getValue() -> T {
        value
    }
}

let integerBox = Box(value: 100)
let stringBox = Box(value: "Swift")

print(integerBox.getValue())
print(stringBox.getValue())

Swift infers the type parameter from the supplied value. Box<Int> and Box<String> are different specializations of the same generic structure.

17. Generic Classes and Reusable Containers

Classes can also declare type parameters. This is useful when a reference type needs to manage values of a particular type.

class Storage<T> {
    private var items: [T] = []

    func add(_ item: T) {
        items.append(item)
    }

    func getAll() -> [T] {
        items
    }
}

let namesStorage = Storage<String>()
namesStorage.add("Amit")
namesStorage.add("Riya")

print(namesStorage.getAll())

Generic containers can be used for cached results, temporary collections, reusable data-management components, and test utilities.

18. Generic Constraints

Sometimes a generic function requires more than just any type. A generic constraint specifies that a type must conform to a protocol or inherit from a particular class. This enables the function to use the operations guaranteed by that requirement.

18.1 Using Comparable

func largerValue<T: Comparable>(_ a: T, _ b: T) -> T {
    return a > b ? a : b
}

print(largerValue(10, 20))
print(largerValue("Apple", "Orange"))

The Comparable constraint allows the function to compare values with operators such as >. Without this constraint, Swift cannot assume that arbitrary types support ordering comparisons.

18.2 Using Equatable

func areEqual<T: Equatable>(_ a: T, _ b: T) -> Bool {
    a == b
}

print(areEqual(10, 10))       // true
print(areEqual("Swift", "iOS")) // false

The Equatable protocol allows values of a type to be compared for equality.

18.3 Multiple Constraints

func compareAndDescribe<T: Comparable & CustomStringConvertible>(
    _ first: T,
    _ second: T
) -> String {
    if first > second {
        return "\(first) is greater than \(second)"
    }

    if first < second {
        return "\(first) is less than \(second)"
    }

    return "\(first) equals \(second)"
}

Multiple protocol constraints let a generic function use capabilities provided by more than one protocol.

19. The where Clause in Generics

A where clause expresses additional requirements for generic parameters or associated types. It is helpful when the constraints need to be more specific than the declaration alone can express.

func printMatchingValues<T: Sequence>(
    _ first: T,
    _ second: T
) where T.Element: Equatable {
    let matchingValues = first.filter { value in
        second.contains(value)
    }

    print(matchingValues)
}

printMatchingValues([1, 2, 3, 4], [3, 4, 5, 6])
// Output: [3, 4]

The sequence element type must conform to Equatable, enabling the containment check. The function demonstrates how constraints make generic code more expressive while retaining type safety.

20. Combining Closures and Generics

Closures and generics work well together. A generic function can accept a closure that defines how values should be processed, allowing one reusable function to support many data types and behaviors.

func transform<Input, Output>(
    _ value: Input,
    using operation: (Input) -> Output
) -> Output {
    operation(value)
}

let numberResult = transform(5) { $0 * 10 }
let textResult = transform("swift") { $0.uppercased() }

print(numberResult) // 50
print(textResult)   // SWIFT

The function accepts any input type and returns whatever output type the supplied closure produces. This is useful for reusable transformation utilities, data conversion, and application-level helper functions.

21. Practical iOS Example: Filtering and Sorting Products

The following example combines custom data types, higher-order functions, closures, and generic constraints to process product data.

struct Product {
    let name: String
    let price: Double
    let isAvailable: Bool
}

let products = [
    Product(name: "Keyboard", price: 1200, isAvailable: true),
    Product(name: "Mouse", price: 600, isAvailable: true),
    Product(name: "Monitor", price: 8500, isAvailable: false),
    Product(name: "Headphones", price: 1800, isAvailable: true)
]

let availableProducts = products
    .filter { $0.isAvailable }
    .sorted { $0.price < $1.price }

let productNames = availableProducts.map { $0.name }

print(productNames)
// Output: ["Mouse", "Keyboard", "Headphones"]

This pipeline selects available products, sorts them by price, and transforms the result into an array of names. Such logic can support a shopping screen, product catalogue, or search-results page.

22. Applying Closures in SwiftUI

SwiftUI uses closures throughout its declarative interface APIs. A closure can describe what happens when a user taps a button or how a view changes in response to state.

import SwiftUI

struct CounterView: View {
    @State private var count = 0

    var body: some View {
        VStack(spacing: 16) {
            Text("Count: \(count)")

            Button("Increase") {
                count += 1
            }

            Button("Reset") {
                count = 0
            }
        }
        .padding()
    }
}

The button actions are closures. They execute when the corresponding button is tapped. The @State property allows SwiftUI to update the view when the count changes.

23. Closures in Asynchronous Operations

Closures are frequently used as completion handlers for operations that finish later. The handler may be called after a network request, file operation, or other asynchronous task completes.

func loadData(completion: @escaping (String) -> Void) {
    DispatchQueue.global().async {
        let message = "Data loaded"

        DispatchQueue.main.async {
            completion(message)
        }
    }
}

loadData { message in
    print(message)
}

This simplified example performs work on a background queue and calls the completion closure on the main queue. In an actual application, network errors, cancellation, and response validation should also be handled. Modern Swift code can often use async/await for asynchronous workflows instead of deeply nested completion handlers.

24. Common Errors and Troubleshooting

Problem Possible Cause Solution
Cannot infer closure parameter type The compiler has insufficient type information. Add explicit parameter types or provide a more specific expected function type.
Missing return value A closure expected to return a value does not return a compatible result. Return a value of the expected type on every required path.
Escaping closure requires annotation The closure is stored or used after its receiving function returns. Declare the closure parameter with @escaping when appropriate.
Strong reference cycle A stored closure strongly retains the object that stores it. Review ownership and consider [weak self] where suitable.
Generic type mismatch Arguments or return values do not satisfy the generic type requirements. Check inferred types and use separate type parameters when types may differ.
Generic operation is unavailable The generic type lacks a required capability. Add a suitable protocol constraint such as Comparable or Equatable.
Unexpected collection results The wrong transformation or filtering method was chosen. Check the closure's condition, return type, and the order of chained operations.

25. Best Practices

  • Use descriptive parameter names when a closure contains complex logic.
  • Prefer concise closures for simple transformations and comparisons.
  • Use trailing closure syntax when it improves the readability of a function call.
  • Break long transformation pipelines into named intermediate values when doing so clarifies intent.
  • Use map for transformation, filter for selection, and reduce for accumulation.
  • Choose compactMap when optional transformation results should be discarded.
  • Use generic constraints to document and enforce the capabilities required by reusable code.
  • Avoid making generic APIs more complicated than the problem requires.
  • Review capture lists and object lifetimes for escaping closures.
  • Do not assume a callback runs immediately, exactly once, or on a particular thread unless the API guarantees it.
  • Use the main thread or main actor for UI updates, following the concurrency model of the API being used.
  • Test edge cases, including empty arrays, invalid input, duplicate values, and boundary conditions.

26. Practical Exercises

  1. Basic closure: Create a closure that accepts two integers and returns their average as a Double.
  2. Map: Convert an array of temperatures from Celsius to Fahrenheit using map.
  3. Filter: Select all strings longer than five characters from an array.
  4. Reduce: Calculate the total price of a list of products.
  5. Sorting: Sort an array of student records by marks in descending order.
  6. compactMap: Convert an array of numeric strings into integers while ignoring invalid entries.
  7. Generic function: Write a generic function that returns the first element of a non-empty array as an optional.
  8. Generic structure: Create a generic container that stores a value and allows it to be replaced.
  9. Protocol constraint: Write a generic function that finds the smaller of two comparable values.
  10. Combined project: Build a product-list processor that filters available products, sorts them by price, and returns their names.

27. Interview Questions and Answers

Q1. What is a closure in Swift?

A closure is a self-contained block of code that can be stored, passed as an argument, and executed when required. It can also capture values from its surrounding scope.

Q2. What is the difference between a function and a closure?

A named function is declared with a function name using func. A closure expression is often anonymous and can be written inline. Functions are also a form of closure in Swift.

Q3. What is trailing closure syntax?

It is a syntax that places a closure argument after a function call's parentheses when the closure is the final argument. It can make calls easier to read.

Q4. What is an escaping closure?

An escaping closure can be stored or invoked after the function that receives it returns. Such parameters are marked with @escaping.

Q5. What does map() do?

map transforms every element of a collection and returns a new collection containing the transformed values.

Q6. How is filter() different from map()?

filter selects existing elements that satisfy a condition, while map transforms each element into a new value.

Q7. What is reduce() used for?

reduce combines the elements of a collection into a single accumulated result using an initial value and a closure.

Q8. What are generics in Swift?

Generics allow functions and types to work with different data types while preserving type safety and reducing duplicated code.

Q9. What is a generic constraint?

A generic constraint restricts a type parameter to types that satisfy a requirement, such as conformance to Comparable or Equatable.

Q10. Can closures and generics be used together?

Yes. A generic function can accept a closure that defines a transformation or operation, making the function reusable for different input and output types.

Q11. Why might a closure use [weak self]?

It can avoid strongly retaining a class instance and help prevent a strong reference cycle when the closure is stored by that instance.

Q12. What is the difference between compactMap() and flatMap() for sequences?

compactMap removes nil results from a transformation. Sequence flatMap can transform each element into a sequence and flatten the resulting nested sequences.

28. Quick Revision Checklist

  • Understand closure parameters, return types, and the in keyword.
  • Recognize type inference, implicit returns, shorthand arguments, and trailing closures.
  • Understand captured values, capture lists, escaping closures, and reference cycles.
  • Use map, filter, reduce, sorted, compactMap, and flatMap appropriately.
  • Create generic functions and generic structures using type parameters.
  • Apply protocol constraints and where clauses when generic code needs specific capabilities.
  • Combine closures and generics to create reusable data-processing utilities.
  • Practice SwiftUI button actions and asynchronous completion handlers.

29. Conclusion

Closures, higher-order functions, and generics are fundamental tools for writing modern Swift applications. Closures encapsulate behavior, higher-order functions simplify collection processing, and generics support reusable code without losing type safety. Together, these features help developers build clearer data transformations, reusable utilities, responsive interfaces, and maintainable iOS applications.

Practice these concepts in small Swift programs before applying them to larger projects. Pay particular attention to type inference, closure lifetime, memory management, protocol constraints, and the readability of chained operations.

30. Course Resources

Previous Next
whatsapp